/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jmeter.save;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.List;

import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
import org.junit.jupiter.api.Test;

public class TestSaveService extends JMeterTestCase {

    // testLoadAndSave test files
    private static final String[] FILES = new String[] {
        "AssertionTestPlan.jmx",
        "AuthManagerTestPlan.jmx",
        "HeaderManagerTestPlan.jmx",
        "InterleaveTestPlan2.jmx",
        "InterleaveTestPlan.jmx",
        "LoopTestPlan.jmx",
        "Modification Manager.jmx",
        "OnceOnlyTestPlan.jmx",
        "proxy.jmx",
        "ProxyServerTestPlan.jmx",
        "SimpleTestPlan.jmx",
        "GuiTest.jmx",
        "GuiTest231.jmx",
        // autogenerated test files
        "GenTest27.jmx", // 2.7
        "GenTest210.jmx", // 2.10
        "GenTest2_13.jmx", // 2.13
        "GenTest3_0.jmx", // 3.0
        };

    // Test files for testLoadAndSave; output will generally be different in size but same number of lines
    private static final String[] FILES_LINES = new String[] {
        "GuiTest231_original.jmx",
        "GenTest25.jmx", // GraphAccumVisualizer obsolete, BSFSamplerGui now a TestBean
        "GenTest251.jmx", // GraphAccumVisualizer obsolete, BSFSamplerGui now a TestBean
        "GenTest26.jmx", // GraphAccumVisualizer now obsolete
        "GenTest27_original.jmx", // CTT changed to use intProp for mode
    };

    // Test files for testLoad; output will generally be different in size and line count
    private static final String[] FILES_LOAD_ONLY = new String[] {
        "GuiTest_original.jmx",
        "GenTest22.jmx",
        "GenTest231.jmx",
        "GenTest24.jmx",
        };

    private static final boolean saveOut = JMeterUtils.getPropDefault("testsaveservice.saveout", false);


    @Test
    public void testPROPVERSION() {
        assertEquals(
                SaveService.PROPVERSION,
                SaveService.getPropertyVersion(),
                "Property Version mismatch, ensure you update SaveService#PROPVERSION field with _version property value from saveservice.properties"
        );
    }

    @Test
    public void testLoadAndSave() throws Exception {
        boolean failed = false; // Did a test fail?

        for (final String fileName : FILES) {
            final File testFile = findTestFile("testfiles/" + fileName);
            final File savedFile = findTestFile("testfiles/Saved" + fileName);
            failed |= loadAndSave(testFile, fileName, true, savedFile);
        }
        for (final String fileName : FILES_LINES) {
            final File testFile = findTestFile("testfiles/" + fileName);
            final File savedFile = findTestFile("testfiles/Saved" + fileName);
            failed |= loadAndSave(testFile, fileName, false, savedFile);
        }
        if (failed) // TODO make these separate tests?
        {
            fail("One or more failures detected");
        }
    }

    private boolean loadAndSave(File testFile, String fileName, boolean checkSize, File savedFile) throws Exception {

        boolean failed = false;

        final FileStats origStats = getFileStats(testFile);
        final FileStats savedStats = getFileStats(savedFile);

        ByteArrayOutputStream out = new ByteArrayOutputStream(Math.toIntExact(testFile.length()));
        try {
            HashTree tree = SaveService.loadTree(testFile);
            SaveService.saveTree(tree, out);
        } finally {
            out.close(); // Make sure all the data is flushed out
        }

        final FileStats compareStats = savedStats == FileStats.NO_STATS ? origStats : savedStats;

        final FileStats outputStats;
        try (ByteArrayInputStream ins = new ByteArrayInputStream(out.toByteArray());
             Reader insReader = new InputStreamReader(ins, Charset.defaultCharset());
             BufferedReader bufferedReader = new BufferedReader(insReader)) {
            outputStats = computeFileStats(bufferedReader);
        }
        // We only check the length of the result. Comparing the
        // actual result (out.toByteArray==original) will usually
        // fail, because the order of the properties within each
        // test element may change. Comparing the lengths should be
        // enough to detect most problem cases...
        if (checkSize && !compareStats.isSameSize(outputStats) || !compareStats.hasSameLinesCount(outputStats)) {
            failed = true;
            System.out.println();
            System.out.println("Loading file testfiles/" + fileName + " and "
                    + "saving it back changes its size from " + compareStats.size + " to " + outputStats.size + ".");
            if (!origStats.hasSameLinesCount(outputStats)) {
                System.out.println("Number of lines changes from " + compareStats.lines + " to " + outputStats.lines);
            }
            if (saveOut) {
                final File outFile = findTestFile("testfiles/" + fileName + ".out");
                System.out.println("Write " + outFile);
                try (FileOutputStream outf = new FileOutputStream(outFile)) {
                    outf.write(out.toByteArray());
                }
                System.out.println("Wrote " + outFile);
            }
        }

        // Note this test will fail if a property is added or
        // removed to any of the components used in the test
        // files. The way to solve this is to appropriately change
        // the test file.
        return failed;
    }

    private FileStats getFileStats(File testFile) throws IOException,
            FileNotFoundException {
        if (testFile == null || !testFile.exists()) {
            return FileStats.NO_STATS;
        }
        try (FileInputStream fis = new FileInputStream(testFile);
             InputStreamReader fileReader = new InputStreamReader(fis, Charset.defaultCharset());
                BufferedReader bufferedReader = new BufferedReader(fileReader)) {
            return computeFileStats(bufferedReader);
        }
    }

    /**
     * Calculate size and line count ignoring EOL and
     * "jmeterTestPlan" element which may vary because of
     * different attributes/attribute lengths.
     */
    private FileStats computeFileStats(BufferedReader br) throws IOException {
        int length = 0;
        int lines = 0;
        String line;
        while ((line = br.readLine()) != null) {
            lines++;
            if (!line.startsWith("<jmeterTestPlan")) {
                length += line.length();
            }
        }
        return new FileStats(length, lines);
    }

    @Test
    public void testLoad() throws Exception {
        for (String fileName : FILES_LOAD_ONLY) {
            File file = findTestFile("testfiles/" + fileName);
            try {
                HashTree tree = SaveService.loadTree(file);
                assertNotNull(tree);
            } catch (IllegalArgumentException ex) {
                ex.addSuppressed(new Throwable("fileName=" + file.getAbsolutePath()));
                throw ex;
            }
        }
    }

    @Test
    public void testClasses(){
        List<String> missingClasses = SaveService.checkClasses();
        if (missingClasses.size() > 0) {
            fail("One or more classes not found:"+missingClasses);
        }
    }

    private static class FileStats {
        int size;
        int lines;

        final static FileStats NO_STATS = new FileStats(-1, -1);

        public FileStats(int size, int lines) {
            this.size = size;
            this.lines = lines;
        }

        public boolean isSameSize(FileStats other) {
            if (other == null) {
                return false;
            }
            return size == other.size;
        }

        public boolean hasSameLinesCount(FileStats other) {
            if (other == null) {
                return false;
            }
            return lines == other.lines;
        }
    }
}
